﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Generic;
using System.Windows;

namespace MvvmFx.Common.ViewModels.Behaviors.Observable
{
    public class CollectionObserver : CollectionObserver<ViewModelBase>
    {
        public CollectionObserver(ViewModelCollection<ViewModelBase> collection)
            : base(collection)
        {

        }
    }

    /// <summary>
    /// Monitors the PropertyChanged event of an object that implements INotifyPropertyChanged,
    /// and executes callback methods (i.e. handlers) registered for properties of that object.
    /// </summary>
    /// <typeparam name="TPropertySource">The type of object to monitor for property changes.</typeparam>
    public partial class CollectionObserver<TPropertySource> : IWeakEventListener
        where TPropertySource : ViewModelBase
    {
        #region Fields

        private readonly ViewModelCollection<TPropertySource> _collection;
        readonly Dictionary<string, Action<TPropertySource>> _propertyNameToHandlerMap;
        readonly List<WeakReference> _propertySourceRefs;

        #endregion // Fields

        #region Constructor

        /// <summary>
        /// Initializes a new instance of PropertyObserver, which
        /// observes the 'propertySource' object for property changes.
        /// </summary>
        /// <param name="collection">The object to monitor for property changes.</param>
        public CollectionObserver(ViewModelCollection<TPropertySource> collection)
        {
            _collection = collection;
            if (object.ReferenceEquals(null, collection))
                throw new ArgumentNullException("collection");

            _collection.ItemAdded += CollectionItemAdded;
            _collection.ItemRemoved += CollectionItemRemoved;

            //_propertySourceRef = new WeakReference(collection);
            _propertySourceRefs = new List<WeakReference>();
            _propertyNameToHandlerMap = new Dictionary<string, Action<TPropertySource>>();

            foreach (var item in collection)
            {
                _propertySourceRefs.Add(new WeakReference(item));
            }
        }

        #endregion // Constructor

        #region Add/Remove items in the collection

        void CollectionItemAdded(object sender, ItemEventArgs<TPropertySource> e)
        {
            _propertySourceRefs.Add(new WeakReference(e.Item));
        }

        #endregion

        #region Public Methods

        #region AddHandler

        public CollectionObserver<TPropertySource> AddHandler(
            string propertyName,
            Action<TPropertySource> handler)
        {
            if (String.IsNullOrEmpty(propertyName))
                throw new ArgumentException("property name is null or empty");

            if (handler == null)
                throw new ArgumentNullException("handler");

            _propertyNameToHandlerMap[propertyName] = handler;

            _propertySourceRefs.ForEach(weakRef =>
                                            {
                                                var propertySource = (TPropertySource)weakRef.Target;
                                                PropertyChangedEventManager.AddListener(propertySource, this, propertyName);
                                            });
            return this;
        }

        /// <summary>
        /// Registers a callback to be invoked when the PropertyChanged event has been raised for the specified property.
        /// </summary>
        /// <param name="expression">A lambda expression like 'n => n.PropertyName'.</param>
        /// <param name="handler">The callback to invoke when the property has changed.</param>
        /// <returns>The object on which this method was invoked, to allow for multiple invocations chained together.</returns>
        public CollectionObserver<TPropertySource> AddHandler(
            Expression<Func<TPropertySource, object>> expression,
            Action<TPropertySource> handler)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            string propertyName = this.GetPropertyName(expression);
            if (String.IsNullOrEmpty(propertyName))
                throw new ArgumentException("'expression' did not provide a property name.");

            if (handler == null)
                throw new ArgumentNullException("handler");

            _propertyNameToHandlerMap[propertyName] = handler;

            _propertySourceRefs.ForEach(weakRef =>
                                            {
                                                var propertySource = (TPropertySource)weakRef.Target;
                                                PropertyChangedEventManager.AddListener(propertySource, this, propertyName);
                                            });

            return this;
        }

        #endregion // AddHandler

        #region RemoveHandler

        /// <summary>
        /// Removes the callback associated with the specified property.
        /// </summary>
        /// <param name="propertyName">A lambda expression like 'n => n.PropertyName'.</param>
        /// <returns>The object on which this method was invoked, to allow for multiple invocations chained together.</returns>
        public CollectionObserver<TPropertySource> RemoveHandler(Expression<Func<TPropertySource, object>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            string propertyName = this.GetPropertyName(expression);
            if (String.IsNullOrEmpty(propertyName))
                throw new ArgumentException("'expression' did not provide a property name.");

            if (_propertyNameToHandlerMap.ContainsKey(propertyName))
            {
                _propertyNameToHandlerMap.Remove(propertyName);

                _propertySourceRefs.ForEach(weakRef =>
                                                {
                                                    var propertySource = (TPropertySource)weakRef.Target;
                                                    PropertyChangedEventManager.RemoveListener(propertySource, this, propertyName);
                                                });
            }

            return this;
        }

        #endregion // RemoveHandler

        #endregion // Public Methods

        #region Private Helpers

        #region GetPropertyName

        string GetPropertyName(Expression<Func<TPropertySource, object>> expression)
        {
            var lambda = expression as LambdaExpression;
            MemberExpression memberExpression;
            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = lambda.Body as UnaryExpression;
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
            else
            {
                memberExpression = lambda.Body as MemberExpression;
            }

            Debug.Assert(memberExpression != null, "Please provide a lambda expression like 'n => n.PropertyName'");

            if (memberExpression != null)
            {
                var propertyInfo = memberExpression.Member as PropertyInfo;

                return propertyInfo.Name;
            }

            return null;
        }

        #endregion // GetPropertyName

        #endregion // Private Helpers

        #region IWeakEventListener Members

        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            if (managerType == typeof(PropertyChangedEventManager))
            {
                string propertyName = ((PropertyChangedEventArgs)e).PropertyName;
                TPropertySource propertySource = (TPropertySource)sender;

                if (String.IsNullOrEmpty(propertyName))
                {
                    // When the property name is empty, all properties are considered to be invalidated.
                    // Iterate over a copy of the list of handlers, in case a handler is registered by a callback.
                    foreach (Action<TPropertySource> handler in _propertyNameToHandlerMap.Values.ToArray())
                        handler(propertySource);

                    return true;
                }
                else
                {
                    Action<TPropertySource> handler;
                    if (_propertyNameToHandlerMap.TryGetValue(propertyName, out handler))
                    {
                        handler(propertySource);
                        return true;
                    }
                }
            }

            return false;
        }

        #endregion
    }
}